import java.io.*;
import java.util.*;
import entries.Entry;

public class CodeWriter {

	// Contains the addresses of all the declared constants and variables and the arguments used in procedures and functions
	private Map<String, Integer> addresses;
	// Contains the types of all the declared constants and variables and the arguments used in procedures and functions
	private Map<String, Character> types;
	// Contains all return types for functions
	private Map<String, Character> functions;
	// The amount of items on a stack
	private int stack;
	
	// Sentences to print on screen, when reading values and when an error occurs
    //Request an integer
	private String digit = "ENTER DIGIT:";		
    //Request a character
    private String letter = "ENTER LETTER:";
	//Request a boolean
    private String bool = "ENTER TRUE (1) or FALSE (0):";
	//Error message
	private String error_div = "Trying to divide by 0, which isn't allowed. The program will now exit.";
	private String error_mod = "Trying to change to modulo 0, which isn't allowed. The program will now exit.";
	
	/** Writer which writes to the file */
	private BufferedWriter out;
	
	
	/**
	 * Write TAM code to a file, based on the code generation
	 * Start the program
	 */
	public CodeWriter(){
		addresses = new TreeMap<String, Integer>();
		types = new TreeMap<String, Character>();
		functions = new TreeMap<String, Character>();
		stack = 0;
		try{
			out = new BufferedWriter(new FileWriter("obj.tasm"));
			out.write("TAM assembler code generated by CodeGenerator!\n");
		} catch (IOException ioe) {
			System.err.println("Cannot create file");
		}
	}
	
	/**
	 * Return the type of an identifier
	 * @param id The name of the identifier
	 * @return The type belonging to identifier id
	 */
	public char getType(String id){ return types.get(id); }

	/** End of program */
	public void endProgram(){
		try{
			if(stack != 0){
				out.write("\t\tPOP(0)\t\t"+stack+"\t\t;\tRemove variables\n");
			}
			out.write("\t\tHALT\t\t\t\t;\tend of program");
			out.flush();
			out.close();
		} catch (IOException ioe) {
			System.err.println("Cannot close file");
		}
	}
	
	/**
	 * Declare a constant
	 * @param id The name of the constant
	 * @param type The type of the constant
	 */
	public void constantDeclaration(String id, Character type, int addr){
		addresses.put(id, addr);
		types.put(id, type);
		stack++;
		try {
			out.write("\t\tSTORE(1)\t"+addr+"[SB]\t;\t"+id+" @ "+addr+"[SB]\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public void free(String id, int addr){
		try {
			out.write("\t\tPUSH\t\t1\t\t;\t"+id+" @ "+addr+"[SB]\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Declare a variable
	 * @param id The name of the variable
	 * @param type The type of the variable
	 */
	public void variableDeclaration(String id, String type, int addr){
		addresses.put(id, addr);
		types.put(id, Entry.type(type));
		stack++;
		try {
			out.write("\t\tPUSH\t\t1\t\t;\t"+id+" @ "+addr+"[SB]\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Create a new procedure
	 * @param id The name of the procedure
	 * @param pars The parameters the procedure has
	 */
	public void procedure_1(String id, List<String> pars, int l1, int l2){
		for(int i = 0; i<pars.size(); i++){
			addresses.put(pars.get(i), (i-pars.size()));
		}
		addresses.put(id, l1);
		try {
			out.write("\t\tJUMP\t\tL"+l2+"[CB]\t;\tSkip the procedure, only perform it when called\n");
			out.write("L"+addresses.get(id)+":\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Second part of a procedure
	 * @param length The amount of arguments the procedure has
	 * @param stats Amount of statements in the procedure, which need to be removed
	 */
	public void procedure_2(String id, int length, int stats, int l1){
		try {
			out.write("\t\tLOADL\t\t1\t\t;\tLoad 1 as dummy value\n");
			out.write("\t\tPOP(1)\t\t"+stats+"\t\t;\tRemove the result of all unused exressions.\n");
			out.write("\t\tRETURN(1)\t"+length+"\t\t;\tReturn to the place where the procedure was called and dump the arguments\n");
			out.write("L"+l1+":\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
		for(String entry: addresses.keySet()){
			if(addresses.get(entry) < 0){
				types.remove(entry);
			}
		}
	}
	
	/**
	 * Create a new function
	 * @param id The name of the function
	 * @param type The return type of the function
	 * @param pars The parameters the function has
	 */
	public void function_1(String id, String type, List<String> pars, int l1, int l2){
		functions.put(id, Entry.type(type));
		for(int i = 0; i<pars.size(); i++){
			addresses.put(pars.get(i), (i-pars.size()));
		}
		addresses.put(id, l2);
		try {
			out.write("\t\tJUMP\t\tL"+l1+"[CB]\t;\tSkip the procedure, only perform it when called\n");
			out.write("L"+addresses.get(id)+":\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Second part of creating a function
	 * @param pars The amount of parameters that a function has, which need to be cleaned up when returning
	 * @param stats Amount of statements in the function (minus the result), which need to be removed
	 */
	public void function_2(int pars, int stats, int l1){
		try {
			out.write("\t\tPOP(1)\t\t"+stats+"\t\t;\tRemove the result of all unused exressions.\n");
			out.write("\t\tRETURN(1)\t"+pars+"\t\t;\tReturn to the place where the procedure was called and dump the arguments\n");
			out.write("L"+l1+":\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
		for(String entry: addresses.keySet()){
			if(addresses.get(entry) < 0){
				types.remove(entry);
			}
		}
	}
	
	/**
	 * Put a new parameter in the Map of parameters
	 * @param id The name of the parameter
	 * @param type The type of the parameter
	 */
	public void parameter(String id, String type){
		types.put(id, Entry.type(type)); 
	}
	
	/** Expression, POP the result from the stack after the expression, the last result is never used */
	public void expression(){
		try{
			out.write("\t\tPOP(0)\t\t1\t\t;\tRemove the result of the expression because it hasn't been used.\n");
		}catch(IOException e){
			e.printStackTrace();
		}
	}
	
	/**
	 * Assign a value to an identifier and put it up on the stack again
	 * @param id The name of the identifier
	 */
	public void assignment(String id){
		try {
			if(addresses.get(id) >= 0){
				out.write("\t\tLOAD(1)\t\t-1[ST]\t;\tStart an assignment by copying the value\n");
				out.write("\t\tSTORE(1)\t"+addresses.get(id)+"[SB]\t;\tStore a new value for "+id+" @ "+addresses.get(id)+"[SB]\n");
			}else{
				out.write("\t\tLOAD(1)\t\t-1[ST]\t;\tStart an assignment by copying the value\n");
				out.write("\t\tSTORE(1)\t"+addresses.get(id)+"[LB]\t;\tStore a new value for "+id+" @ "+addresses.get(id)+"[LB]\n");
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Perform the not (!) operation */
	public void not(){
		try {
			out.write("\t\tCALL\t\tnot\t\t;\tNot operation\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Perform the addition (+) operation */
	public void addition(){
		try {
			out.write("\t\tCALL\t\tadd\t\t;\tAddition\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Perform the subtraction (-) operation */
	public void subtraction(){
		try {
			out.write("\t\tCALL\t\tsub\t\t;\tSubtraction\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Perform the negation (-) operation */
	public void negation(){
		try {
			out.write("\t\tCALL\t\tneg\t\t;\tNegation\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Perform the multiplication (*) operation */
	public void multiplication(){
		try {
			out.write("\t\tCALL\t\tmult\t;\tMultiply\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Perform the division (/) operation
	 * @param l1
	 * @param l2
	 * @param addr
	 */
	public void divide(int l1, int l2, int addr){
		try {
			out.write("\t\tLOAD(1)\t\t-1[ST]\t;\n");
			out.write("\t\tJUMPIF(0)\tL"+l1+"[CB]\t;\tIf the value is 0 print out an error message, otherwise perform the division\n");
			out.write("\t\tCALL\t\tdiv\t\t;\tDivision\n");
			out.write("\t\tJUMP\t\tL"+l2+"[CB]\t;\n");
			out.write("L"+l1+":\n");
    		for(int i=0; i<error_div.length(); i++){
				out.write("\t\tLOADL\t\t"+((int) error_div.charAt(i))+"\t\t;\n");
				out.write("\t\tCALL\t\tput\t\t;\n");
			}
    		out.write("\t\tHALT\t\t\t\t;\tend of program\n");
    		out.write("L"+l2+":\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Perform the modulo (%) operation */
	public void modulo(int l1, int l2, int addr){
		try {
			out.write("\t\tLOAD(1)\t\t-1[ST]\t;\n");
			out.write("\t\tJUMPIF(0)\tL"+l1+"[CB]\t;\tIf the value is 0 print out an error message, otherwise perform the division\n");
			out.write("\t\tCALL\t\tmod\t\t;\tModulo operation\n");
			out.write("\t\tJUMP\t\tL"+l2+"[CB]\t;\n");
			out.write("L"+l1+":\n");
    		for(int i=0; i<error_mod.length(); i++){
				out.write("\t\tLOADL\t\t"+((int) error_mod.charAt(i))+"\t\t;\n");
				out.write("\t\tCALL\t\tput\t\t;\n");
			}
    		out.write("\t\tHALT\t\t\t\t;\tend of program\n");
    		out.write("L"+l2+":\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Perform the or (||) operation */
	public void or(){
		try {
			out.write("\t\tCALL\t\tor\t\t;\tOr operation\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Perform the and (&&) operation */
	public void and(){
		try {
			out.write("\t\tCALL\t\tand\t\t;\tAnd operation\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Perform the less than (<) operation */
	public void lessThan(){
		try {
			out.write("\t\tCALL\t\tlt\t\t;\t< operation\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Perform the less than or equal to (<=) operation */
	public void lessEqual(){
		try {
			out.write("\t\tCALL\t\tle\t\t;\t<= operation\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Perform the greater than (>) operation */
	public void greaterThan(){
		try {
			out.write("\t\tCALL\t\tgt\t\t;\t> operation\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Perform the greater than or equal to (>=) operation */
	public void greaterEqual(){
		try {
			out.write("\t\tCALL\t\tge\t\t;\t>= operation\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Perform the equals (==) operation */
	public void equals(){
		try {
			out.write("\t\tLOADL\t\t1\t\t;\tAll entries are of length 1\n");
			out.write("\t\tCALL\t\teq\t\t;\t== operation\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Perform the not equals (!=) operation */
	public void notEquals(){
		try {
			out.write("\t\tLOADL\t\t1\t\t;\tAll entries are of length 1\n");
    		out.write("\t\tCALL\t\tne\t\t;\t<> operation\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Write an identifier to the stack
	 * @param id The name of the identifier
	 */
	public void identifier(String id){
		try {
			if(addresses.get(id) >= 0){
				out.write("\t\tLOAD(1)\t\t"+addresses.get(id)+"[SB]\t;\t"+id+"@"+addresses.get(id)+"[SB]\n");
			}else{
				out.write("\t\tLOAD(1)\t\t"+addresses.get(id)+"[LB]\t;\t"+id+"@"+addresses.get(id)+"[LB]\n");
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
		
	/**
	 * Write an integer to the stack
	 * @param n The number to be written
	 */
	public void integer(String n){
		try {
			out.write("\t\tLOADL\t\t"+n+"\t\t;\tPut "+n+" on the stack\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Write a character to the stack
	 * @param c The letter to be written
	 */
	public void character(String c){
		try {
			out.write("\t\tLOADL\t\t"+(int) c.charAt(0)+"\t\t;\tPut "+c+" on the stack\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Write true (1) to the stack	 */
	public void boolean_true(){
		try {
			out.write("\t\tLOADL\t\t1\t\t;\tPut TRUE (1) on the stack\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Write false (0) to the stack	 */
	public void boolean_false(){
		try {
			out.write("\t\tLOADL\t\t0\t\t;\tPut FALSE (0) on the stack\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Finish a compound expression, by popping all expressions/declarations on that level, excluding the last one, which will be the result
	 * @param stats The amount of statements that need to be popped from the stack
	 */
	public void compound(int stats){
		try {
			out.write("\t\tPOP(1)\t\t"+stats+"\t\t;\tRemove stataments, except last, to end a compound expression\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Read from input a letter, number or boolean value
	 * then return either the type that was read (b, c, i) or Entry.NONE if more than one identifier was read
	 * @param id The list of identifiers that will be read into
	 * @return The type of the read expression
	 */
	public char read(List<Object> id){
		char result = Entry.VOID;
		try {
			if(id.size() == 1){
				if(types.get(id.get(0).toString()) == Entry.CHARACTER){
					result = Entry.CHARACTER;
					for(int i=0; i<letter.length(); i++){
						out.write("\t\tLOADL\t\t"+((int) letter.charAt(i))+"\t\t;\n");
						out.write("\t\tCALL\t\tput\t\t;\n");
					}
					out.write("\t\tCALL\t\tputeol\t;\n");
					out.write("\t\tLOADA\t\t"+addresses.get(id.get(0).toString())+"[SB]\t;\n");
					out.write("\t\tCALL\t\tget\t;\tRead one character from the input\n");
					out.write("\t\tCALL\t\tgeteol\t;\tGet the eol from System.in\n");
					out.write("\t\tLOAD(1)\t\t"+addresses.get(id.get(0).toString())+"[SB]\t;\tLoad "+id.get(0).toString()+" onto the stack, in case it needs to be used\n");
				} else if(types.get(id.get(0).toString()) == Entry.INTEGER) {
					result = Entry.INTEGER;
					for(int i=0; i<digit.length(); i++){
						out.write("\t\tLOADL\t\t"+((int) digit.charAt(i))+"\t\t;\n");
						out.write("\t\tCALL\t\tput\t\t;\n");
					}
					out.write("\t\tCALL\t\tputeol\t;\n");
					out.write("\t\tLOADA\t\t"+addresses.get(id.get(0).toString())+"[SB]\t;\n");
					out.write("\t\tCALL\t\tgetint\t;\tRead one digit from the input\n");
					out.write("\t\tCALL\t\tgeteol\t;\tGet the eol from System.in\n");
					out.write("\t\tLOAD(1)\t\t"+addresses.get(id.get(0).toString())+"[SB]\t;\tLoad "+id.get(0).toString()+" onto the stack, in case it needs to be used\n");
				} else {
					result = Entry.BOOLEAN;
					for(int i=0; i<bool.length(); i++){
						out.write("\t\tLOADL\t\t"+((int) bool.charAt(i))+"\t\t;\n");
						out.write("\t\tCALL\t\tput\t\t;\n");
					}
					out.write("\t\tCALL\t\tputeol\t;\n");
					out.write("\t\tLOADA\t\t"+addresses.get(id.get(0).toString())+"[SB]\t;\n");
					out.write("\t\tCALL\t\tgetint\t;\tRead boolean value from the input\n");
					out.write("\t\tCALL\t\tgeteol\t;\tGet the eol from System.in\n");
					out.write("\t\tLOAD(1)\t\t"+addresses.get(id.get(0).toString())+"[SB]\t;\tLoad "+id.get(0).toString()+" onto the stack, in case it needs to be used\n");
				}
			}else{
				for(Object obj: id){
					String text = obj.toString();
					if(types.get(text) == Entry.CHARACTER){
						for(int i=0; i<letter.length(); i++){
							out.write("\t\tLOADL\t\t"+((int) letter.charAt(i))+"\t\t;\n");
							out.write("\t\tCALL\t\tput\t\t;\n");
						}
						out.write("\t\tCALL\t\tputeol\t;\n");
						out.write("\t\tLOADA\t\t"+addresses.get(text)+"[SB]\t;\n");
						out.write("\t\tCALL\t\tget\t\t;\tRead one character from the input\n");
						out.write("\t\tCALL\t\tgeteol\t;\tGet the eol from System.in\n");
					} else if(types.get(text) == Entry.INTEGER) {
						for(int i=0; i<digit.length(); i++){
							out.write("\t\tLOADL\t\t"+((int) digit.charAt(i))+"\t\t;\n");
							out.write("\t\tCALL\t\tput\t\t;\n");
						}
						out.write("\t\tCALL\t\tputeol\t;\n");
						out.write("\t\tLOADA\t\t"+addresses.get(text)+"[SB]\t;\n");
						out.write("\t\tCALL\t\tgetint\t;\tRead one digit from the input\n");
						out.write("\t\tCALL\t\tgeteol\t;\tGet the eol from System.in\n");
					} else {
						for(int i=0; i<bool.length(); i++){
							out.write("\t\tLOADL\t\t"+((int) bool.charAt(i))+"\t\t;\n");
							out.write("\t\tCALL\t\tput\t\t;\n");
						}
						out.write("\t\tCALL\t\tputeol\t;\n");
						out.write("\t\tLOADA\t\t"+addresses.get(text)+"[SB]\t;\n");
						out.write("\t\tCALL\t\tgetint\t;\tRead boolean value from the input\n");
						out.write("\t\tCALL\t\tgeteol\t;\tGet the eol from System.in\n");
					}
				}
				out.write("\t\tLOADL\t\t1\t\t;\t\n");
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return result;
	}
		
	/**
	 * Print an expression to the output
	 * @param cnt The number of which expression this is (used only for the first expression)
	 * @param type The type of the expression being printed (used only for the first expression)
	 * @return The type of the first expression (if there is only one) and otherwise Entry.VOID
	 */
	public char print(int cnt, char type){
		char result = Entry.VOID;
		try {
			if(cnt == 1){
				result = type;
				out.write("\t\tLOAD(1)\t\t-1[ST]\t;\tCopy the expression result\n");
			}
			if(type == Entry.BOOLEAN || type == Entry.INTEGER){
				out.write("\t\tCALL\t\tputint\t;\tPrint the value onto the output\n");
				out.write("\t\tCALL\t\tputeol\t;\tEnd the line\n");
			}else if(type == Entry.CHARACTER){
				out.write("\t\tCALL\t\tput\t;\tPrint the value onto the output\n");
				out.write("\t\tCALL\t\tputeol\t;\tEnd the line\n");
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return result;
	}
	
	/**
	 * Call a procedure/function
	 * @param id The name of the function to be called
	 */
	public void call(String id){
		try {
			out.write("\t\tCALL(SB)\tL"+addresses.get(id)+"[CB]\t;\tCall the procedure\n"); 
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Get the return type of a function
	 * @param id The name of the function
	 * @return The type belonging to function id
	 */
	public char getFunctionType(String id) { return functions.get(id); }
	
	/**
	 * Start a conditional expression
	 */
	public void conditional_1(int l1){
		try {
			out.write("\t\tJUMPIF(0)\tL"+l1+"[CB]\t;\tGo to L"+l1+" if expression yields FALSE\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * The if part, pop all declarations
	 * @param stats The amount of expressions in the then part of the conditional expression
	 */
	public void conditional_2(int stats, int l1, int l2){
		try {
			if(stats > 0){
				out.write("\t\tPOP(1)\t\t"+stats+"\t\t;\tRemove all expressions, they cannot be used anymore\n");
			}
			out.write("\t\tJUMP\t\tL"+l2+"[CB]\t;\tGo to L"+l2+" after evaluating the compound expression\n");
			out.write("L"+l1+":\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * The else part and the ending of a conditional expression, pop all declarations in both parts
	 * @param stats The amount of statements in the else part of the conditional expression
	 * @param l2 the second label number
	 */
	public void conditional_3(int stats, int l2){
		try {
			if(stats > 0){
				out.write("\t\tPOP(1)\t\t"+stats+"\t\t;\tRemove all declarations/expressions, they cannot be used anymore\n");
			}
			out.write("L"+l2+":\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * The else part and the ending of a conditional expression, pop all declarations in both parts
	 * @param l2 the second label number
	 */
	public void conditional_4(int l2){
		try {
			out.write("L"+l2+":\n");
			out.write("\t\tLOADL\t\t1\t\t;\t\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** Start a while expression, the while part */
	public void dowhile_1(int l1){
		try {
			out.write("L"+l1+":\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** 
	 * Continue a while expression, the do part 
	 *	@param stats the amount of statements expressions in the compound expression
	 */
	public void dowhile_2(int stats, int l2){
		try {
			out.write("\t\tJUMPIF(0)\tL"+l2+"[CB]\t;\tGo to L"+l2+" if the expression yields FALSE\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Finish the while expression and pop all declarations
	 * @param stats The amount of statements in the while expression
	 */
	public void dowhile_3(int stats, int l1, int l2){
		try {
			out.write("\t\tPOP(0)\t\t"+stats+"\t;\tRemove all statements, they cannot be used anymore\n");
			out.write("\t\tJUMP\t\tL"+l1+"[CB]\t;\tRe-evaluate the expression at L"+l1+"\n");
			out.write("L"+l2+":\n");
			out.write("\t\tLOADL\t\t1\t\t;\t\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
